/*
 *  This software may only be used by you under license from AT&T Corp.
 *  ("AT&T").  A copy of AT&T's Source Code Agreement is available at
 *  AT&T's Internet website having the URL:
 *  <http://www.research.att.com/sw/tools/graphviz/license/source.html>
 *  If you received this software without first entering into a license
 *  with AT&T, you have an infringing copy of this software and cannot use
 *  it without violating AT&T's intellectual property rights.
 */

package att.grappa;

import java.lang.reflect.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.net.*;
import java.util.HashMap;
import java.util.Map;

/**
 * This class brings together shape, text and attribute information
 * related to bounding and drawing an element.
 *
 * @version 1.2, ; Copyright 1996 - 2010 by AT&T Corp.
 * @author  <a href="mailto:john@research.att.com">John Mocenigo</a>, <a href="http://www.research.att.com">Research @ AT&T Labs</a>
 */
public class GrappaNexus
    implements
	GrappaConstants,
	Cloneable, ImageObserver, Shape, IGrappaObjectListener
{
	static Map<String,Font> FontCache = new HashMap<String,Font>() ;
	
    /**
     * RoundRectangle arc height factor
     */
    public static double arcHeightFactor = 0.05;

    /**
     * RoundRectangle arc width factor
     */
    public static double arcWidthFactor = 0.05;


    static {
        if (Grappa.toolkit == null) {
            try {
                Grappa.toolkit = java.awt.Toolkit.getDefaultToolkit();
            }
            catch(Throwable err) {
            }
        }
    }

    Area textArea = null;
    Shape shape = null;
    int shapeType = NO_SHAPE;
    Rectangle2D bbox = null;
    GrappaStyle style = null;
    Color fillcolor = null;
    Color color = null;
    Image image = null;
    boolean imageLoading = false;

    boolean dirty = false; // just for cluster subgraphs, now

    Stroke stroke = null;

    // used for RECORD_SHAPE/MRECORD_SHAPE only, so far
    private Object[] objs = null;

    // used when SHAPE_ATTR is CUSTOM_SHAPE
    private Object custom_shape = null;

    /**
     * Indicates if element text should be included in the element
     * bounding box. By default, it is.
     *
     * @see Grappa#shapeBoundText
     */
    public boolean boundText = true;
    /**
     * Indicates if the area bounding the element text should be
     * filled/outlined along with the element when being drawn.
     * By default, it is not.
     *
     * @see Grappa#shapeClearText
     */
    public boolean clearText = false;
    /**
     * Indicates if element text should be drawn when drawing the
     * element. By default, it is.
     *
     * @see Grappa#shapeDrawText
     */
    public boolean drawText  = true;

    Element element = null;

    long lastUpdate = 0;
    private long lastShapeUpdate = 0;
    private long lastTextUpdate = 0;
    private long lastStyleUpdate = 0;
    private long lastDecorationUpdate = 0;
    private long lastImageUpdate = 0;

    Font font = null;
    String[] lstr = null;
    GrappaPoint[] lpos = null;
    Color font_color = null;

    // fix winding rule at instantiation time
    private int windingRule = Grappa.windingRule;

    ////////////////////////////////////////////////////////////////////////
    //
    // Constructors
    //
    ////////////////////////////////////////////////////////////////////////

    /**
     * Constructs a new <code>GrappaNexus</code> object from an element.
     * @param elem the <code>Element</code> needing a <code>GrappaNexus</code> object.
     */
    public GrappaNexus(Element elem) {
    	this.element = elem;
    	rebuild();
    }

    ////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    ////////////////////////////////////////////////////////////////////////

    /**
     * Get the underlying element.
     *
     * @return the element underlying this GrappaNexus.
     */
    public Element getElement() {
	return element;
    }

    /**
     * Return the image, if any, loaded for this element
     * @return an image or null
     */
    public Image getImage() {
    	return image;
    }

    /**
     * Return status of image loading.
     * Returns true whenever an image has begun loading for this
     * element, but has not yet completed.
     *
     * @return true, during image loading; false, otherwise
     */
    public boolean isImageLoading() {
    	return imageLoading;
    }

    /**
     * Return the winding rule for this line.
     * @return one of WIND_NON_ZERO or WIND_EVEN_ODD
     */
    public int getWindingRule() {
    	return windingRule;
    }

    /**
     * Recompute the components of this GrappaNexus.
     *
     * @see updateStyle
     * @see updateDecoration
     * @see updateShape
     * @see updateText
     * @see updateImage
     */
    public void rebuild() {
    	updateStyle();
    	updateDecoration();
    	updateShape();
    	updateText();
    	updateImage();
    }

    /**
     * Update the shape information for the underlying element.
     * For nodes, the <I>distortion</I>, <I>height</I>, <I>orientation</I>, <I>peripheries</I>, <I>pos</I>, <I>rotation</I>, <I>shape</I>, <I>sides</I>, <I>skew</I> and <I>width</I> attributes are examined.
     * For edges, the <I>pos</I> attribute is examined.
     * For subgraph, the bounding box is recomputed.
     */
    public void updateShape() {
    	long thisShapeUpdate = System.currentTimeMillis();
    	switch(element.getType()) {
    	case NODE:
    		// re-initialize some values
    		custom_shape = null;
    		objs = null;

    		if(element.getSubgraph().isCluster() && element.getSubgraph().grappaNexus != null)
    			element.getSubgraph().grappaNexus.dirty = true;

    		Node node = (Node)element;
    		GrappaPoint pos = (GrappaPoint)node.getAttributeValue(POS_ATTR);
    		Double Width = (Double)node.getAttributeValue(WIDTH_ATTR);
    		Double Height = (Double)node.getAttributeValue(HEIGHT_ATTR);
    		Integer Type = (Integer)node.getAttributeValue(SHAPE_ATTR);
    		double width = PointsPerInch * Width.doubleValue();
    		double height = PointsPerInch * Height.doubleValue();
    		int type = Type.intValue();

    		// the above attributes are sure to be there since they are defaulted,
    		// but these could return null values, so be sure to account for that
    		Integer Peripheries = (Integer)node.getAttributeValue(PERIPHERIES_ATTR);
    		Integer Sides = (Integer)node.getAttributeValue(SIDES_ATTR);
    		Double Distortion = (Double)node.getAttributeValue(DISTORTION_ATTR);
    		Double Skew = (Double)node.getAttributeValue(SKEW_ATTR);
    		Double Orientation = (Double)node.getAttributeValue(ORIENTATION_ATTR);
    		Double Rotation = (Double)node.getAttributeValue(ROTATION_ATTR);

    		int peripheries = Peripheries == null ? -1 : Peripheries.intValue();
    		int sides = Sides == null ? -1 : Sides.intValue();
    		double distortion = Distortion == null ? 0 : Distortion.doubleValue();
    		double skew = Skew == null ? 0 : Skew.doubleValue();
    		double orientation = Orientation == null ? 0 : Orientation.doubleValue();
    		double rotation = Rotation == null ? 0 : Rotation.doubleValue();

    		if(Orientation != null && orientation != 0 && Grappa.orientationInDegrees) {
    			orientation = Math.PI * orientation / 180.0;
    		}

    		GeneralPath path;
    		switch(type) {
    		case CUSTOM_SHAPE:
    			String custom = (String)node.getAttributeValue(CUSTOM_ATTR);
    			if(custom == null) {
    				throw new IllegalArgumentException("custom attibuted null for node (" + node.getName() + ") with custom shape");
    			}
    			Class custom_class;
    			try {
    				custom_class = Class.forName(custom);
    			}
    			catch(Exception e) {
    				throw new IllegalArgumentException("custom class unavailable for custom shape '" + custom + "'");
    			}
    			if(!(GrappaShape.class.isAssignableFrom(custom_class))) {
    				throw new IllegalArgumentException("custom class '" + custom + "' does not extend the GrappaShape class");
    			}
    			Constructor ccustom;
    			try {
    				ccustom= custom_class.getConstructor(new Class[] { Element.class, double.class, double.class, double.class, double.class });
    			}
    			catch(Exception e) {
    				throw new IllegalArgumentException("constructor for custom class shape '" + custom + "' not found");
    			}
    			try {
    				if(Grappa.centerPointNodes) {
    					shape = (Shape)(custom_shape = ccustom.newInstance(new Object[] { node,  new Double(pos.x - (width/2.0)), new Double(pos.y - (height/2.0)), new Double(width), new Double(height) }));
    				} else {
    					shape = (Shape)(custom_shape = ccustom.newInstance(new Object[] { node, new Double(pos.x), new Double(pos.y), new Double(width), new Double(height) }));
    				}
    			}
    			catch(Exception e) {
    				if(e instanceof InvocationTargetException) {
    					Throwable t = ((InvocationTargetException)e).getTargetException();
    					Grappa.displayException((Exception)t);
    				} else if(e instanceof UndeclaredThrowableException) {
    					throw new IllegalArgumentException("cannot instantiate custom shape '" + custom + "' for node '" + node.getName() + "' because2: " + ((UndeclaredThrowableException)e).getUndeclaredThrowable().getMessage());
    				} else {
    					throw new IllegalArgumentException("cannot instantiate custom shape '" + custom + "' for node '" + node.getName() + "' because3: " + e.getMessage());
    				}
    			}
    			shapeType = CUSTOM_SHAPE;
    			break;
    		case BOX_SHAPE:
    			if(
    					(Distortion == null || distortion == 0)
    					&&
    					(Skew == null || skew == 0)
    					&&
    					(Orientation == null || orientation == 0)
    			) {
    				shapeType = BOX_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new Rectangle2D.Double(pos.x - (width/2.0), pos.y - (height/2.0), width, height);
    				} else {
    					shape = new Rectangle2D.Double(pos.x, pos.y, width, height);
    				}
    				if(Peripheries != null && peripheries > 1) {
    					path = new GeneralPath(shape);
    					for(int i = 1; i < peripheries; i++) {
    						if(Grappa.centerPointNodes) {
    							path.append
    							(
    									new Rectangle2D.Double
    									(
    											(pos.x - width/2.0) + (double)(i * PERIPHERY_GAP),
    											(pos.y - height/2.0) + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP)
    									),
    									false
    							);
    						} else {
    							path.append
    							(
    									new Rectangle2D.Double
    									(
    											pos.x + (double)(i * PERIPHERY_GAP),
    											pos.y + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP)
    									),
    									false
    							);
    						}
    					}
    					shape = path;
    				}
    			} else {
    				shapeType = BOX_SHAPE|GRAPPA_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new GrappaShape(shapeType, pos.x, pos.y, width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				} else {
    					shape = new GrappaShape(shapeType, pos.x + (width/2.0), pos.y + (height/2.0), width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				}
    			}
    			break;
    		case ROUNDEDBOX_SHAPE:
    			if(
    					(Distortion == null || distortion == 0)
    					&&
    					(Skew == null || skew == 0)
    					&&
    					(Orientation == null || orientation == 0)
    			) {
    				shapeType = ROUNDEDBOX_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new RoundRectangle2D.Double(pos.x - (width/2.0), pos.y - (height/2.0), width, height, arcWidthFactor * width, arcHeightFactor * height);
    				} else {
    					shape = new RoundRectangle2D.Double(pos.x, pos.y, width, height, arcWidthFactor * width, arcHeightFactor * height);
    				}
    				if(Peripheries != null && peripheries > 1) {
    					path = new GeneralPath(shape);
    					for(int i = 1; i < peripheries; i++) {
    						if(Grappa.centerPointNodes) {
    							path.append
    							(
    									new RoundRectangle2D.Double
    									(
    											(pos.x - width/2.0) + (double)(i * PERIPHERY_GAP),
    											(pos.y - height/2.0) + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP),
    											arcWidthFactor * (width - (double)(2 * i * PERIPHERY_GAP)),
    											arcHeightFactor * (height - (double)(2 * i * PERIPHERY_GAP))
    									),
    									false
    							);
    						} else {
    							path.append
    							(
    									new RoundRectangle2D.Double
    									(
    											pos.x + (double)(i * PERIPHERY_GAP),
    											pos.y + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP),
    											arcWidthFactor * (width - (double)(2 * i * PERIPHERY_GAP)),
    											arcHeightFactor * (height - (double)(2 * i * PERIPHERY_GAP))
    									),
    									false
    							);
    						}
    					}
    					shape = path;
    				}
    			} else {
    				shapeType = ROUNDEDBOX_SHAPE|GRAPPA_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new GrappaShape(shapeType, pos.x, pos.y, width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				} else {
    					shape = new GrappaShape(shapeType, pos.x + (width/2.0), pos.y + (height/2.0), width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				}
    			}
    			break;
    		case OVAL_SHAPE:
    			if(
    					(Distortion == null || distortion == 0)
    					&&
    					(Skew == null || skew == 0)
    					&&
    					(Orientation == null || orientation == 0)
    			) {
    				shapeType = OVAL_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new Ellipse2D.Double(pos.x - (width/2.0), pos.y - (height/2.0), width, height);
    				} else {
    					shape = new Ellipse2D.Double(pos.x, pos.y, width, height);
    				}
    				if(Peripheries != null && peripheries > 1) {
    					path = new GeneralPath(shape);
    					for(int i = 1; i < peripheries; i++) {
    						if(Grappa.centerPointNodes) {
    							path.append
    							(
    									new Ellipse2D.Double
    									(
    											(pos.x - width/2.0) + (double)(i * PERIPHERY_GAP),
    											(pos.y - height/2.0) + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP)
    									),
    									false
    							);
    						} else {
    							path.append
    							(
    									new Ellipse2D.Double
    									(
    											pos.x + (double)(i * PERIPHERY_GAP),
    											pos.y + (double)(i * PERIPHERY_GAP),
    											width - (double)(2 * i * PERIPHERY_GAP),
    											height - (double)(2 * i * PERIPHERY_GAP)
    									),
    									false
    							);
    						}
    					}
    					shape = path;
    				}
    			} else {
    				shapeType = OVAL_SHAPE|GRAPPA_SHAPE;
    				if(Grappa.centerPointNodes) {
    					shape = new GrappaShape(shapeType, pos.x, pos.y, width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				} else {
    					shape = new GrappaShape(shapeType, pos.x + (width/2.0), pos.y + (height/2.0), width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    				}
    			}
    			break;
    		case DIAMOND_SHAPE:
    		case DOUBLECIRCLE_SHAPE:
    		case DOUBLEOCTAGON_SHAPE:
    		case EGG_SHAPE:
    		case HEXAGON_SHAPE:
    		case HOUSE_SHAPE:
    		case INVERTEDHOUSE_SHAPE:
    		case INVERTEDTRAPEZIUM_SHAPE:
    		case INVERTEDTRIANGLE_SHAPE:
    		case OCTAGON_SHAPE:
    		case PARALLELOGRAM_SHAPE:
    		case PENTAGON_SHAPE:
    		case PLAINTEXT_SHAPE:
    		case POINT_SHAPE:
    		case POLYGON_SHAPE:
    		case TRAPEZIUM_SHAPE:
    		case TRIANGLE_SHAPE:
    		case TRIPLEOCTAGON_SHAPE:
    		case MCIRCLE_SHAPE:
    		case MDIAMOND_SHAPE:
    		case MSQUARE_SHAPE:
    			shapeType = type|GRAPPA_SHAPE;
    			if(Grappa.centerPointNodes) {
    				shape = new GrappaShape(shapeType, pos.x, pos.y, width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    			} else {
    				shape = new GrappaShape(shapeType, pos.x + (width/2.0), pos.y + (height/2.0), width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, null);
    			}
    			break;
    		case RECORD_SHAPE:
    		case MRECORD_SHAPE:
    			shapeType = type|GRAPPA_SHAPE;
    			objs = GrappaSupportRects.parseRecordInfo(node);
    			String rects = null;
    			if(objs != null)
    				rects = (String)(objs[2]);
    			if(Grappa.centerPointNodes) {
    				shape = new GrappaShape(shapeType, pos.x, pos.y, width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, rects);
    			} else {
    				shape = new GrappaShape(shapeType, pos.x + (width/2.0), pos.y + (height/2.0), width, height, sides, peripheries, distortion, skew, orientation, style.rounded, style.diagonals, rects);
    			}
    			break;
    		default:
    			throw new IllegalArgumentException("unsupported type for this constructor (" + type + ")");
    		}

    		// handle rotation (rotation just spins the node,
    		// orientation spins it within a fixed bounding box
    		if(Rotation != null && rotation != 0 && shape != null) {
    			double theta = rotation;
    			if(Grappa.rotationInDegrees) {
    				theta = Math.PI * theta / 180.0;
    			}
    			if(Grappa.centerPointNodes)
    				shape = AffineTransform.getRotateInstance(theta,pos.x,pos.y).createTransformedShape(shape);
    			else
    				shape = AffineTransform.getRotateInstance(theta,pos.x+(width/2.0),pos.y+(height/2.0)).createTransformedShape(shape);
    		}
    		break;
    	case EDGE:
    		Edge edge = (Edge)element;
    		shapeType = LINE_SHAPE;

    		if(element.getSubgraph().isCluster() && element.getSubgraph().grappaNexus != null)
    			element.getSubgraph().grappaNexus.dirty = true;

    		if((shape = (Shape)edge.getAttributeValue(POS_ATTR)) == null) {
    			Integer attr_type = (Integer)(edge.getAttributeValue(DIR_ATTR));

    			edge.direction = (attr_type != null ? attr_type.intValue() : (edge.getGraph().isDirected()?GrappaLine.TAIL_ARROW_EDGE:GrappaLine.NONE_ARROW_EDGE));

    			// create a default straight line connecting the two
    			// node centers
    			edge.setAttribute(
    					POS_ATTR,
    					new GrappaLine(new GrappaPoint[] { (GrappaPoint)(edge.getTail().getAttributeValue(POS_ATTR)), (GrappaPoint)(edge.getHead().getAttributeValue(POS_ATTR)) }, edge.direction)
    			);
    			shape = (Shape)edge.getAttributeValue(POS_ATTR);
    		}
    		break;
    	case SUBGRAPH:
    		Subgraph subgraph = (Subgraph)element;
    		shapeType = BOX_SHAPE;

    		dirty = false;

    		// cannot call subgraph.getBoundingBox() because it would recurse,
    		// so just put the guts here
    		Rectangle2D sgbox = null;
    		Element elem = null;
    		GraphIterator enm = subgraph.elements();
    		while(enm.hasNext()) {
    			elem = enm.nextGraphElement();
    			if(elem == element) continue;
    			switch(elem.getType()) {
    			case Grappa.NODE:
    			case Grappa.EDGE:
    				elem.buildShape();
    				if(sgbox == null) {
    					sgbox = elem.grappaNexus.getBounds2D();
    				} else {
    					sgbox.add(elem.grappaNexus.rawBounds2D());
    				}
    				break;
    			case Grappa.SUBGRAPH:
    				if(sgbox == null) {
    					sgbox = ((Subgraph)elem).getBoundingBox();
    				} else {
    					sgbox.add(((Subgraph)elem).getBoundingBox());
    				}
    				break;
    			default: // cannot happen
    				throw new InternalError("unknown type (" + elem.getType() + ")");
    			}
    		}
    		GrappaSize minSize = (GrappaSize)element.getAttributeValue(MINSIZE_ATTR);
    		if(minSize != null) {
    			if(sgbox == null) {
    				sgbox = new java.awt.geom.Rectangle2D.Double(0,0,minSize.getWidth(),minSize.getHeight());
    			} else {
    				sgbox.add(new java.awt.geom.Rectangle2D.Double(sgbox.getCenterX()-(minSize.getWidth()/2.0),sgbox.getCenterY()-(minSize.getHeight()/2.0),minSize.getWidth(),minSize.getHeight()));
    			}
    		}
    		GrappaBox minBox = (GrappaBox)element.getAttributeValue(MINBOX_ATTR);
    		if(minBox != null) {
    			if(sgbox == null) {
    				sgbox = new java.awt.geom.Rectangle2D.Double(minBox.x,minBox.y,minBox.width,minBox.height);
    			} else {
    				sgbox.add(new java.awt.geom.Rectangle2D.Double(minBox.x,minBox.y,minBox.width,minBox.height));
    			}
    		}
    		if(sgbox == null) {
    			sgbox = new java.awt.geom.Rectangle2D.Double(0,0,0,0);
    		}
    		shape = sgbox;
    		break;
    	default:
    		throw new IllegalArgumentException("unrecognized element type (" + element.getType() + ") for " + element.getName());
    	}

    	bboxCheckSet();
    	lastUpdate = lastShapeUpdate = thisShapeUpdate;
    }

    /**
     * Update the shape information for the underlying element.
     * The <I>style</I> attribute is examined.
     */
    public void updateStyle() {
	long thisStyleUpdate = System.currentTimeMillis();
	if((style = (GrappaStyle)element.getAttributeValue(STYLE_ATTR)) == null) {
	    throw new InternalError("style defaults not properly set in Graph.java");
	}

	// an attempt to handle font info passed via style instead of fontstyle
	if(
	   style.font_style != null
	   &&
	   style.font_style != (Integer)element.getAttributeValue(FONTSTYLE_ATTR)
	   ) {
	    element.setAttribute(FONTSTYLE_ATTR,style.font_style);
	    style.font_style = null;
	}
	lastUpdate = lastStyleUpdate = thisStyleUpdate;
    }
   
    /**
     * Update the text information for the underlying element.
     * The <I>fontcolor</I>, <I>fontname</I>, <I>fontsize</I>, <I>fontstyle</I>, and <I>label</I> attributes are examined.
     * The <I>lp</I> attribute is also examined for edges and subgraphs.
     */
    public void updateText() {
    	String[] tstr = null;
    	GrappaPoint[] tpos = null;
    	Area area = null;
    	Font tfont = null;
    	boolean makeAdjustment = false;
    	double signum = -1;
    	int offset = 0;
    	String headstr, tailstr;
    	GrappaPoint headpt, tailpt;
    	int lcnt = 0;
    	boolean hasEdgeLabel = false;
    	Attribute attr = null;

    	long thisTextUpdate = System.currentTimeMillis();

    	String[] labels;
    	GrappaPoint[] lps;

    	String labelAttr = (String)element.getAttributeValue(LABEL_ATTR);

    	if( "\\N".equals(labelAttr) ) {
    		labelAttr = element.getName();
    	}

    	if(labelAttr != null && labelAttr.length() > 0) {
    		lcnt++;
    	} else labelAttr = null;

    	headstr = tailstr = null;
    	headpt = tailpt = null;

    	if (element.isEdge()) {
    		if ((headstr = (String)element.getAttributeValue(HEADLABEL_ATTR)) != null && (attr = element.getLocalAttribute(HEADLP_ATTR)) != null) {
    			headpt = (GrappaPoint)(attr.getValue());
    			lcnt++;
    			hasEdgeLabel = true;
    		} else headstr = null;
    		if ((tailstr = (String)element.getAttributeValue(TAILLABEL_ATTR)) != null && (attr = element.getLocalAttribute(TAILLP_ATTR)) != null) {
    			tailpt = (GrappaPoint)(attr.getValue());
    			lcnt++;
    			hasEdgeLabel = true;
    		} else tailstr = null;
    	}

    	// all string attributes are trimmed when they are stored
    	// if(labelAttr != null)
    	//     labelAttr = labelAttr.trim();

    	if(labelAttr != null || hasEdgeLabel) {
    		if(
    				element.isNode()
    				&&
    				(
    						shapeType == (RECORD_SHAPE|GRAPPA_SHAPE)
    						||
    						shapeType == (MRECORD_SHAPE|GRAPPA_SHAPE)
    				)
    				&&
    				labelAttr.indexOf('|') < 0
    				&&
    				labelAttr.indexOf('{') == 0
    				&&
    				labelAttr.lastIndexOf('}') == labelAttr.length() - 1
    		) {
    			labelAttr = labelAttr.substring(1,labelAttr.length()-1).trim();
    		}

    		if(hasEdgeLabel || labelAttr.length() > 0) {

    			String fontname = (String)element.getAttributeValue(FONTNAME_ATTR);
    			Integer fontstyle = (Integer)element.getAttributeValue(FONTSTYLE_ATTR);
    			Number fontsize = (Number)element.getAttributeValue(FONTSIZE_ATTR);
    			Number fontadj = (Number)(element.getGraph()).getGrappaAttributeValue(GRAPPA_FONTSIZE_ADJUSTMENT_ATTR);

    			// set font
    			String fontKey = fontname + "_" + fontstyle.intValue() + "_" + (float)(fontsize.floatValue()+fontadj.floatValue()) ;
    			tfont = FontCache.get( fontKey ) ;
    			if( tfont == null ) {
    				//tfont = new Font(fontname,fontstyle.intValue(),fontsize.intValue() + fontadj.intValue());
    				tfont = new Font(fontname,fontstyle.intValue(),fontsize.intValue() + fontadj.intValue()).deriveFont( (float) fontsize.floatValue() ) ;
    				FontCache.put( fontKey, tfont ) ;
    			}
    			//String rectString = null;

    			int lines;
    			int i;
    			char[] array;
    			int[] justification;
    			Rectangle2D[] bnds;
    			java.awt.font.LineMetrics[] mtrc;
    			int start;
    			char ch;
    			//String str;
    			double wdinfo, htinfo;
    			double top;
    			double x;

    			if(
    					element.isNode()
    					&&
    					(
    							shapeType == (RECORD_SHAPE|GRAPPA_SHAPE)
    							||
    							shapeType == (MRECORD_SHAPE|GRAPPA_SHAPE)
    					)
    					&&
    					labelAttr.indexOf('|') >= 0
    			) {
    				if(objs == null)
    					updateShape();
    				if(objs != null && objs[0] != null && objs[1] != null) {
    					labels = (String[])objs[0];
    					lps = (GrappaPoint[])objs[1];
    				} else {
    					labels = new String[1];
    					labels[0] = labelAttr;
    					lps = new GrappaPoint[1];
    					lps[0] = ((Node)element).getCenterPoint();
    				}
    			} else {
    				Subgraph sg;

    				labels = new String[lcnt];
    				lps = new GrappaPoint[lcnt];

    				lcnt = 0;
    				if (labelAttr != null)
    					labels[lcnt++] = labelAttr;
    				if (headstr != null) {
    					labels[lcnt] = headstr;
    					lps[lcnt] = headpt;
    					lcnt++;
    				}
    				if (tailstr != null) {
    					labels[lcnt] = tailstr;
    					lps[lcnt] = tailpt;
    				}

    				if (labelAttr != null) {
    					if(Grappa.autoPositionNodeLabel && element.isNode()) {
    						lps[0] = ((Node)element).getCenterPoint();
    					} else if((attr = element.getLocalAttribute(LP_ATTR)) == null || ((sg = element.getSubgraph()) != null && attr == sg.getLocalAttribute(LP_ATTR))) {
    						Rectangle2D lbox;

    						if((lbox = (Rectangle2D)element.getAttributeValue(BBOX_ATTR)) == null) {
    							lbox = bbox;
    						}

    						if(element.isSubgraph() && lbox != null) {
    							lps[0] = new GrappaPoint(lbox.getX() + lbox.getWidth()/2.0, (Grappa.labelGraphBottom?lbox.getMaxY():lbox.getMinY()));
    							element.setAttribute(LP_ATTR, lps[0].clone());
    						} else {
    							lps[0] = null;
    						}
    					} else {
    						lps[0] = (GrappaPoint)(attr.getValue());
    					}
    					if(element.isSubgraph() && attr == null) {
    						if(Grappa.labelGraphBottom) {
    							signum = 1;
    						} else {
    							signum = -1;
    						}
    						makeAdjustment = true;
    					}
    				}
    			}

    			for(int l = 0; l < labels.length; l++) {
    				if(labels[l] != null && lps[l] != null && labels[l].length() > 0) {

    					if(labels[l].equals("\\N")) {
    						labels[l] = element.getName();
    						if(labels[l] == null) continue;
    					}

    					// break label into multiple lines as indicated by line-breaks
    					lines = 1;
    					array = labels[l].toCharArray();

    					// first count lines
    					for(i=0; i<array.length; i++) {
    						if(
    								array[i] == '\\'
    									&&
    									++i < array.length
    									&&
    									(array[i] == 'l' || array[i] == 'r' || array[i] == 'n')
    									&&
    									(i+1) < array.length
    						) {
    							lines++;
    						}
    					}

    					if(tpos == null) {
    						offset = 0;
    						tpos = new GrappaPoint[lines];
    						tstr = new String[lines];
    					} else {
    						offset = tpos.length;
    						GrappaPoint[] p = new GrappaPoint[offset+lines];
    						String[] s = new String[offset+lines];
    						System.arraycopy(tpos,0,p,0,offset);
    						System.arraycopy(tstr,0,s,0,offset);
    						tpos = p;
    						tstr = s;
    					}
    					justification = new int[lines];
    					bnds = new Rectangle2D[lines];
    					mtrc = new java.awt.font.LineMetrics[lines];

    					// now extract lines and justification info
    					lines = 0;
    					start = 0;
    					ch = 'n';
    					//str = null;
    					wdinfo = htinfo = 0;
    					for(i=0; i<array.length; i++) {
    						if(
    								array[i] == '\\'
    									&&
    									++i < array.length
    									&&
    									((ch = array[i]) == 'l' || array[i] == 'r' || array[i] == 'n')
    						) {
    							tstr[offset+lines] = new String(array,start,i-1-start);
    							bnds[lines] = tfont.getStringBounds(tstr[offset+lines],element.getGraph().REFCNTXT);
    							mtrc[lines] = tfont.getLineMetrics(tstr[offset+lines],element.getGraph().REFCNTXT);
    							if(bnds[lines].getWidth() > wdinfo) wdinfo = bnds[lines].getWidth();
    							//htinfo += bnds[lines].getHeight();
    							htinfo += 2 + tfont.getSize();
    							if(ch == 'l') justification[lines++] = -1;
    							else if(ch == 'r') justification[lines++] = 1;
    							else justification[lines++] = 0;
    							start = (i+1);
    						}
    					}
    					if(start < array.length) {
    						tstr[offset+lines] = new String(array,start,array.length - start);
    						bnds[lines] = tfont.getStringBounds(tstr[offset+lines],element.getGraph().REFCNTXT);
    						mtrc[lines] = tfont.getLineMetrics(tstr[offset+lines],element.getGraph().REFCNTXT);
    						if(bnds[lines].getWidth() > wdinfo) wdinfo = bnds[lines].getWidth();
    						//htinfo += bnds[lines].getHeight();
    						htinfo += 2 + tfont.getSize();
    						if(ch == 'l') justification[lines] = -1;
    						else if(ch == 'r') justification[lines] = 1;
    						else justification[lines] = 0;
    					}

    					//htinfo += mtrc[lines].getLeading();
    					//htinfo += (mtrc[lines].getLeading()) * tfont.getSize2D() / mtrc[lines].getHeight();

    					if(makeAdjustment) {
    						if(Grappa.labelGraphOutside) {
    							lps[l].y += signum * htinfo;
    						} else {
    							lps[l].y -= signum * htinfo;
    						}
    					}

    					// half these as that's how they will be used
    					wdinfo /= 2.0;
    					htinfo /= 2.0;

    					// figure out textArea and positioning of each line of text
    					// doing it now instead of at rendering time means some
    					// approximation, but text rendering is iffy anyway and this
    					// will be close enough and more efficient (if you call this
    					// efficient)

    					// first, find top of text bounding box
    					top = lps[l].y - htinfo;

    					// now, for each line:
    					// 1. determine left-side position and create (add to) textArea
    					// 2. getAscent() to determine actually draw position
    					// 3. shift down hieght of line
    					x = 0;
    					for(i = 0; i < bnds.length; i++) {
    						if(justification[i] < 0) {
    							// left
    							x = lps[l].x - wdinfo;
    						} else if(justification[i] > 0) {
    							// right
    							x = lps[l].x + wdinfo - bnds[i].getWidth();
    						} else {
    							x = lps[l].x - bnds[i].getWidth()/2.0;
    						}
    						bnds[i].setRect(x,top,bnds[i].getWidth(),bnds[i].getHeight());
    						if(area == null) {
    							area = new Area(bnds[i]);
    						} else {
    							area.add(new Area(bnds[i]));
    						}
    						//tpos[offset+i] = new GrappaPoint(x, top + mtrc[i].getAscent());
    						//tpos[offset+i] = new GrappaPoint(x, top + Math.ceil((mtrc[i].getAscent()+mtrc[i].getLeading())*tfont.getSize()/mtrc[i].getHeight()));
    						tpos[offset+i] = new GrappaPoint(x, top + tfont.getSize() - 1);

    						//top += bnds[i].getHeight();
    						top += 2 + tfont.getSize();
    					}
    				}
    			}
    		}
    	}

    	// commit changes
    	font = tfont;
    	lpos = tpos;
    	lstr = tstr;
    	textArea = area;
    	bboxCheckSet();
    	lastUpdate = lastTextUpdate = thisTextUpdate;
    }

    /**
     * Update the decoration information for the underlying element.
     * The <I>color</I> and <I>fontcolor</I> attributes are examined.
     * For edges, the <I>dir</I> attribute is examined.
     */
    public void updateDecoration() {
    	long thisDecorationUpdate = System.currentTimeMillis();
    	color = (Color)(element.getAttributeValue(COLOR_ATTR));
    	fillcolor = (Color)(element.getAttributeValue(FILLCOLOR_ATTR));
    	font_color = (Color)(element.getAttributeValue(FONTCOLOR_ATTR));
    	if(element.isEdge() && shape != null && shape instanceof GrappaLine) {
    		Edge edge = (Edge)element;
    		int graph_dir = edge.getGraph().isDirected() ? GrappaLine.TAIL_ARROW_EDGE : GrappaLine.NONE_ARROW_EDGE;
    		int dir = graph_dir;
    		Integer attr_type = (Integer)(edge.getThisAttributeValue(DIR_ATTR));
    		if(attr_type != null)
    			dir = attr_type.intValue(); 

    		edge.direction = dir;

    		GrappaLine gline = (GrappaLine)shape;
    		boolean forward = gline.startsNear((Point2D)(edge.getTail().getAttributeValue(POS_ATTR))); 
    		// basically, it edge loops on same node, assume it is always
    		// in the forward orientation
    		if(!forward && edge.getHead() == edge.getTail())
    			forward = true;

    		int line_dir;
    		if(forward) {
    			line_dir = gline.getArrowType();
    		} else {
    			switch(gline.getArrowType()) {
    			case GrappaLine.HEAD_ARROW_EDGE:
    				line_dir = GrappaLine.TAIL_ARROW_EDGE;
    			case GrappaLine.TAIL_ARROW_EDGE:
    				line_dir = GrappaLine.HEAD_ARROW_EDGE;
    				break;
    			default:
    				line_dir = gline.getArrowType();
    				break;
    			}
    		}
    		if(line_dir != dir) {
    			if(forward) {
    				line_dir = dir;
    			} else {
    				switch(dir) {
    				case GrappaLine.HEAD_ARROW_EDGE:
    					line_dir = GrappaLine.TAIL_ARROW_EDGE;
    				case GrappaLine.TAIL_ARROW_EDGE:
    					line_dir = GrappaLine.HEAD_ARROW_EDGE;
    					break;
    				default:
    					line_dir = dir;
    					break;
    				}
    			}
    			gline.changeArrowType(line_dir);
    			edge.setAttribute(POS_ATTR, gline);
    		}
    	}
    	lastUpdate = lastDecorationUpdate = thisDecorationUpdate;
    }

    /**
     * Update the image information for the underlying element.
     */
    public void updateImage() {
    	long thisImageUpdate = System.currentTimeMillis();
    	String path = (String)(element.getAttributeValue(IMAGE_ATTR));

    	if(path != null && Grappa.toolkit != null) {

    		this.image = null;
    		imageLoading = true;

    		Image raw_image = null;

    		try {
    			URL url = new URL(path);
    			raw_image = Grappa.toolkit.getImage(url);
    		}
    		catch(Exception ex) {}

    		if(raw_image == null) {
    			try {
    				raw_image = Grappa.toolkit.getImage(path);
    			}
    			catch(Exception ex) {}
    		}

    		if(raw_image != null) {
    			if(Grappa.toolkit.prepareImage(raw_image,-1,-1,this)) {
    				this.image = raw_image;
    				imageLoading = false;
    			}
    		} else {
    			imageLoading = false;
    		}
    	} else {
    		this.image = null;
    		imageLoading = false;
    	}

    	lastUpdate = lastImageUpdate = thisImageUpdate;
    }

    public final boolean
    imageUpdate(Image image, int flags, int x, int y, int width, int height) {

    	boolean ret = true;

    	synchronized(this) {
    		if((flags&ALLBITS) == ALLBITS) {
    			ret = false;
    			this.image = image;
    			imageLoading = false;
    			notifyAll();
    		} else if((flags&(ABORT|ERROR)) != 0) {
    			ret = false;
    			imageLoading = false;
    			notifyAll();
    		}
    	}

    	return(ret);
    }

    ////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    ////////////////////////////////////////////////////////////////////////

    private void bboxCheckSet() {
    	Rectangle2D oldbox = bbox;
    	bbox = null;
    	Rectangle2D newbox = null;
    	try {
    		newbox = rawBounds2D();
    	}
    	catch(Exception ex) {
    		throw (RuntimeException)(ex.fillInStackTrace());
    	}
    	finally {
    		bbox = oldbox;
    	}

    	if(newbox == null) {
    		if(element.isSubgraph() && ((Subgraph)element).countOfElements(SUBGRAPH|NODE|EDGE) == 0) {
    			newbox = new Rectangle2D.Double();
    		} else {
    			throw new InternalError("new bounding box of \"" + element.getName() + "\" is null");
    		}
    	}
    	if(
    			(oldbox == null && newbox != null)
    			||
    			(oldbox != null && newbox == null)
    			||
    			(newbox != null && !newbox.equals(oldbox))
    	) {
    		// bounding box has changed so null out existing bboxes of enclosing subgraphs
    		Subgraph prnt = element.getSubgraph();
    		while(prnt != null) {
    			if(prnt.grappaNexus != null) {
    				prnt.grappaNexus.bbox = null;
    			}
    			prnt = prnt.getSubgraph();
    		}

    		// commit
    		bbox = newbox;
    		lastUpdate = System.currentTimeMillis();
    	}
    }

    ////////////////////////////////////////////////////////////////////////
    //
    // Cloneable interface
    //
    ////////////////////////////////////////////////////////////////////////
 
    /**
     * Creates a new object of the same class as this object.
     *
     * @return     a clone of this instance.
     * @exception  OutOfMemoryError            if there is not enough memory.
     * @see        java.lang.Cloneable
     */
    public Object clone() {
    	try {
    		GrappaNexus copy = (GrappaNexus) super.clone();
    		if(shape != null) {
    			if(shapeType == LINE_SHAPE) {
    				copy.shape = (Shape) ((GrappaLine)shape).clone();
    			} else if(shapeType == BOX_SHAPE) {
    				copy.shape = (Shape) ((Rectangle2D)shape).clone();
    			} else if(shapeType == ROUNDEDBOX_SHAPE) {
    				copy.shape = (Shape) ((RoundRectangle2D)shape).clone();
    			} else if(shapeType == OVAL_SHAPE) {
    				copy.shape = (Shape) ((Ellipse2D)shape).clone();
    			} else if((shapeType&GRAPPA_SHAPE) != 0) {
    				copy.shape = (Shape) ((GrappaShape)shape).clone();
    			} else {
    				copy.shape = (Shape) ((GeneralPath)shape).clone();
    			}
    		}
    		if(textArea != null) {
    			copy.textArea = (Area) textArea.clone();
    		}
    		return copy;
    	} catch (CloneNotSupportedException e) {
    		// this shouldn't happen, since we are Cloneable
    		throw new InternalError();
    	}
    }

    ////////////////////////////////////////////////////////////////////////
    //
    // Shape interface
    //
    ////////////////////////////////////////////////////////////////////////

    public boolean contains(double x, double y) {

    	boolean contains = false;

    	if(shape != null) {
    		contains = shape.contains(x, y);
    	}

    	if(
    			textArea != null && !contains && !clearText && drawText
    			&&
    			(
    					(element.isNode() && element.getGraph().getShowNodeLabels())
    					||
    					(element.isEdge() && element.getGraph().getShowEdgeLabels())
    					||
    					(element.isSubgraph() && element.getGraph().getShowSubgraphLabels())
    			)
    	) {
    		contains = textArea.contains(x, y);
    	}

    	return(contains);
    }

    public boolean contains(double x, double y, double width, double height) {

    	boolean contains = false;

    	if(shape != null) {
    		contains = shape.contains(x, y, width, height);
    	}

    	if(
    			textArea != null && !contains && !clearText && drawText
    			&&
    			(
    					(element.isNode() && element.getGraph().getShowNodeLabels())
    					||
    					(element.isEdge() && element.getGraph().getShowEdgeLabels())
    					||
    					(element.isSubgraph() && element.getGraph().getShowSubgraphLabels())
    			)
    	) {
    		contains = textArea.contains(x, y, width, height);
    	}

    	return(contains);
    }

    public boolean contains(Point2D p) {

    	return(contains(p.getX(),p.getY()));
    }

    public boolean contains(Rectangle2D r) {

    	return(contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()));
    }

    public Rectangle getBounds() {

    	return(getBounds2D().getBounds());
    }

    public Rectangle2D getBounds2D() {

    	if(dirty) {
    		bbox = null;
    		updateShape();
    	}

    	if(bbox == null) {

    		if(shape != null) {
    			bbox = shape.getBounds2D();
    		}

    		if(textArea != null && Grappa.shapeBoundText && boundText) {
    			if(bbox == null) {
    				bbox = textArea.getBounds();
    			} else {
    				bbox.add(textArea.getBounds());
    			}
    		}

    		if(bbox.getHeight() == 0)
    			bbox.setRect(bbox.getX(),bbox.getY(),bbox.getWidth(),0.01);
    		if(bbox.getWidth() == 0)
    			bbox.setRect(bbox.getX(),bbox.getY(),0.01,bbox.getHeight());

    	}
    	// return a clone so no one can mess with the real values
    	return((Rectangle2D)(bbox.clone()));
    }

    // variation for use in Grappa
    Rectangle2D rawBounds2D() {

    	if(dirty) {
    		bbox = null;
    		updateShape();
    	}

    	if(bbox == null) {

    		if(shape != null) {
    			bbox = shape.getBounds2D();
    		}

    		if(textArea != null && Grappa.shapeBoundText && boundText) {
    			if(bbox == null) {
    				bbox = textArea.getBounds();
    			} else {
    				bbox.add(textArea.getBounds());
    			}
    		}

    		if(bbox.getHeight() == 0)
    			bbox.setRect(bbox.getX(),bbox.getY(),bbox.getWidth(),0.01);
    		if(bbox.getWidth() == 0)
    			bbox.setRect(bbox.getX(),bbox.getY(),0.01,bbox.getHeight());
    	}
    	return(bbox);
    }

    /**
     * Equivalent to <TT>getPathIterator(null)</TT>.
     *
     * @see getPathIterator(AffineTransform)
     */
    public PathIterator getPathIterator() {
    	return new GrappaPathIterator(this, null);
    }

    public PathIterator getPathIterator(AffineTransform at) {
    	return new GrappaPathIterator(this, at);
    }

    public PathIterator getPathIterator(AffineTransform at, double flatness) {
    	return new FlatteningPathIterator(new GrappaPathIterator(this, at), flatness);
    }

    public boolean intersects(double x, double y, double width, double height) {

    	boolean intersects = false;

    	if(shape != null) {
    		intersects = shape.intersects(x, y, width, height);
    	}

    	if(
    			textArea != null && !intersects && !clearText && drawText
    			&&
    			(
    					(element.isNode() && element.getGraph().getShowNodeLabels())
    					||
    					(element.isEdge() && element.getGraph().getShowEdgeLabels())
    					||
    					(element.isSubgraph() && element.getGraph().getShowSubgraphLabels())
    			)
    	) {
    		intersects = textArea.intersects(x, y, width, height);
    	}

    	return(intersects);
    }

    public boolean intersects(Rectangle2D r) {

    	return(intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()));
    }

    ////////////////////////////////////////////////////////////////////////
    //
    // Observer interface
    //
    ////////////////////////////////////////////////////////////////////////

    /**
     * This method is called whenever the observed object is changed.
     * When certain observed attributes (attributes of interest)
     * are changed, this method will update the GrappaNexus as needed.
     *
     * @param obs the Observable must be an Attribute
     * @param arg either a Long giving the update time of the Attribute as returned by System.getTimeInMillis() or it is a two element Object array, where the first element is a new Attribute to be observed in place of that passed via <I>obs</I> and the second element is the update time of this new Attribute.
     */
    public void update(Object obs, Object arg) {
//System.err.println( getClass().getName() + "::update(...) " + obs ) ;
    	// begin boilerplate
    	if(!(obs instanceof Attribute)) {
    		throw new IllegalArgumentException("expected to be observing attributes only (obs) for \"" + element.getName() + "\"");
    	}
    	Attribute attr = (Attribute)obs;
    	if(arg instanceof Object[]) {
    		Object[] args = (Object[])arg;
    		if(args.length == 2 && args[0] instanceof Attribute && args[1] instanceof Long) {
    			attr.removeListener(this);
    			attr = (Attribute)args[0];
    			attr.addListener(this);
    			// in case we call: super.update(obs,arg)
    			obs = attr;
    			arg = args[1];
    		} else {
    			throw new IllegalArgumentException("badly formated update information for \"" + element.getName() + "\"");
    		}
    	}
    	// end boilerplate


    	// when this object is created it should register with the
    	// appropriate Attributes based on how it was created;
    	// this method  will then see what has been updated
    	// and set flags in this object (and put tokens in an update
    	// stack in Graph is autoUpdate is true) so that the appropriate
    	// parts will be updated before any drawing occurs.
    	if(arg instanceof Long) {

    		String attrName = attr.getName();
    		int attrHash = attr.getNameHash();
    		long thisUpdate = ((Long)arg).longValue() + 1L;

    		if(element == null || !element.reserve()) return;

    		// reset
    		objs = null;

    		switch(element.getType()) {
    		case NODE:
    			if(
    					(POS_HASH == attrHash && POS_ATTR.equals(attrName))
    					||
    					(WIDTH_HASH == attrHash && WIDTH_ATTR.equals(attrName))
    					||
    					(HEIGHT_HASH == attrHash && HEIGHT_ATTR.equals(attrName))
    					||
    					(SHAPE_HASH == attrHash && SHAPE_ATTR.equals(attrName))
    			) {
    				if(lastShapeUpdate < thisUpdate) {
    					updateShape();
    					if(Grappa.autoPositionNodeLabel) {
    						updateText();
    					}
    				}
    			} else if(
    					(LABEL_HASH == attrHash && LABEL_ATTR.equals(attrName))
    					||
    					(
    							!Grappa.autoPositionNodeLabel
    							&&
    							(LP_HASH == attrHash && LP_ATTR.equals(attrName)) // in case it is used
    					)
    					||
    					(FONTSIZE_HASH == attrHash && FONTSIZE_ATTR.equals(attrName))
    					||
    					(FONTNAME_HASH == attrHash && FONTNAME_ATTR.equals(attrName))
    					||
    					(FONTSTYLE_HASH == attrHash && FONTSTYLE_ATTR.equals(attrName))
    			) {
    				if(lastTextUpdate < thisUpdate) {
    					updateText();
    				}
    			} else if(
    					(STYLE_HASH == attrHash && STYLE_ATTR.equals(attrName))
    			) {
    				if(lastStyleUpdate < thisUpdate) {
    					updateStyle();
    				}
    			} else if(
    					(COLOR_HASH == attrHash && COLOR_ATTR.equals(attrName))
    					||
    					(FONTCOLOR_HASH == attrHash && FONTCOLOR_ATTR.equals(attrName))
    			) {
    				if(lastDecorationUpdate < thisUpdate) {
    					updateDecoration();
    				}
    			} else if(
    					(IMAGE_HASH == attrHash && IMAGE_ATTR.equals(attrName))
    			) {
    				if(lastImageUpdate < thisUpdate) {
    					updateImage();
    				}
    			} else {
    				throw new InternalError("update called for \"" + element.getName() + "\" with an unmonitored attribute: " + attrName);
    			}
    			break;
    		case EDGE:
    			if(
    					(POS_HASH == attrHash && POS_ATTR.equals(attrName))
    			) {
    				if(lastShapeUpdate < thisUpdate) {
    					updateShape();
    				}
    			} else if(
    					(LABEL_HASH == attrHash && LABEL_ATTR.equals(attrName))
    					||
    					(LP_HASH == attrHash && LP_ATTR.equals(attrName))
    					||
    					(HEADLABEL_HASH == attrHash && HEADLABEL_ATTR.equals(attrName))
    					||
    					(HEADLP_HASH == attrHash && HEADLP_ATTR.equals(attrName))
    					||
    					(TAILLABEL_HASH == attrHash && TAILLABEL_ATTR.equals(attrName))
    					||
    					(TAILLP_HASH == attrHash && TAILLP_ATTR.equals(attrName))
    					||
    					(FONTSIZE_HASH == attrHash && FONTSIZE_ATTR.equals(attrName))
    					||
    					(FONTNAME_HASH == attrHash && FONTNAME_ATTR.equals(attrName))
    					||
    					(FONTSTYLE_HASH == attrHash && FONTSTYLE_ATTR.equals(attrName))
    			) {
    				if(lastTextUpdate < thisUpdate) {
    					updateText();
    				}
    			} else if(
    					(STYLE_HASH == attrHash && STYLE_ATTR.equals(attrName))
    			) {
    				if(lastStyleUpdate < thisUpdate) {
    					updateStyle();
    				}
    			} else if(
    					(COLOR_HASH == attrHash && COLOR_ATTR.equals(attrName))
    					||
    					(DIR_HASH == attrHash && DIR_ATTR.equals(attrName))
    					||
    					(FONTCOLOR_HASH == attrHash && FONTCOLOR_ATTR.equals(attrName))
    			) {
    				if(lastDecorationUpdate < thisUpdate) {
    					updateDecoration();
    				}
    			} else if(
    					(IMAGE_HASH == attrHash && IMAGE_ATTR.equals(attrName))
    			) {
    				if(lastImageUpdate < thisUpdate) {
    					updateImage();
    				}
    			} else {
    				throw new InternalError("update called for \"" + element.getName() + "\" with an unmonitored attribute: " + attrName);
    			}
    			break;
    		case SUBGRAPH:
    			if(
    					(LABEL_HASH == attrHash && LABEL_ATTR.equals(attrName))
    					||
    					(LP_HASH == attrHash && LP_ATTR.equals(attrName))
    					||
    					(FONTSIZE_HASH == attrHash && FONTSIZE_ATTR.equals(attrName))
    					||
    					(FONTNAME_HASH == attrHash && FONTNAME_ATTR.equals(attrName))
    					||
    					(FONTSTYLE_HASH == attrHash && FONTSTYLE_ATTR.equals(attrName))
    			) {
    				if(lastTextUpdate < thisUpdate) {
    					updateText();
    				}
    			} else if(
    					(STYLE_HASH == attrHash && STYLE_ATTR.equals(attrName))
    			) {
    				if(lastStyleUpdate < thisUpdate) {
    					updateStyle();
    				}
    			} else if(
    					(COLOR_HASH == attrHash && COLOR_ATTR.equals(attrName))
    					||
    					(FONTCOLOR_HASH == attrHash && FONTCOLOR_ATTR.equals(attrName))
    			) {
    				if(lastDecorationUpdate < thisUpdate) {
    					updateDecoration();
    				}
    			} else if(
    					(IMAGE_HASH == attrHash && IMAGE_ATTR.equals(attrName))
    			) {
    				if(lastImageUpdate < thisUpdate) {
    					updateImage();
    				}
    			} else if(
    					(MINBOX_HASH == attrHash && MINBOX_ATTR.equals(attrName))
    					||
    					(MINSIZE_HASH == attrHash && MINSIZE_ATTR.equals(attrName))
    			) {
    				bbox = null;
    			} else {
    				throw new InternalError("update called for \"" + element.getName() + "\" with an unmonitored attribute: " + attrName);
    			}
    			break;
    		}

    		element.release();
    	} else {
    		throw new InternalError("update called for shape of element \"" + element.getName() + "\" without proper format");
    	}
    }

    /**
     * Draw the element using the supplied Graphics2D context.
     *
     * @param g2d the Graphics2D context to be used for drawing
     */
    void draw(java.awt.Graphics2D g2d) {
    	if(shape instanceof CustomRenderer)
    		((CustomRenderer)shape).draw(g2d);
    	else
    		g2d.draw(this);
    }

    /**
     * Fill the element using the supplied Graphics2D context.
     *
     * @param g2d the Graphics2D context to be used for drawing
     */
    void fill(java.awt.Graphics2D g2d) {
    	if(shape instanceof CustomRenderer)
    		((CustomRenderer)shape).fill(g2d);
    	else
    		g2d.fill(this);
    }

    /**
     * Draw the image associated with the IMAGE_ATTR
     * using the supplied Graphics2D context.
     *
     * @param g2d the Graphics2D context to be used for drawing
     */
    void drawImage(java.awt.Graphics2D g2d) {
    	if(Grappa.waitForImages && imageLoading) {
    		synchronized(this) {
    			try {
    				wait();
    			}
    			catch(InterruptedException e) {}
    		}
    	}
    	if(image != null) {
    		if(shape instanceof CustomRenderer) {
    			((CustomRenderer)shape).drawImage(g2d);
    		} else {
    			Rectangle sbox = shape.getBounds();
    			Shape clip = g2d.getClip();
    			g2d.clip(shape);
    			g2d.drawImage(image, sbox.x, sbox.y, sbox.width, sbox.height, null);
    			g2d.setClip(clip);
    		}
    	}
    }
}
